State Object Controller topic
In the Fluttery Framework, the typical controller object extends the SateXController class and contains the business rules for the app. A controller is also used by a particular State object to deal with any event handling. The State object itself deals with just the interface. The State object would typically extend the StateX class, so to utilize the Controller object in its build() function or its other functions. Again, the controller is to work with the app's business rules and or address any events like the pushing of a button.
External State Control | No Controller Bloat | Control The State | The Singleton Pattern | The List of Device and System Events |
External State Control
When a StateX object takes in a StateXController object through its constructor or though one of its add() functions (see screenshot), that controller now has access to that State object and all its properties and functions. That very fact allows for some powerful capabilities. Essentially, you now have the ability to call the State object's setState() function from outside its class---through that controller object! The basic requirement of any State Management in Flutter is to reliably call the setState() function of a specific State object. Controllers makes this easily possible.
page_01.dart |
---|
The two screenshots below demonstrate how easily you can now reference a particular State object from yet another State object if need be. The code below is from the example app that accompanies the Fluttery Framework package. The first screenshot is of the State class, Page2State, which displays 'Page 2' of the Three-Page Counter Example app. In this app, you're able to increment the counter of even a neighboring page with a tap of a button.
The first screenshot shows how to increment the counter on 'Page 1' from the 'Page 2' screen. The second screenshot shows how to increment the counter on 'Page 2' from the 'Page 3' screen. A simple demonstration, but a spectacular one if you think about it! Notice it's the same controller class object (unimaginatively named Controller) being used, and it can reference all three State objects at the same time!
page_02.dart | page_03.dart |
---|
During the course of a typical app, as the user progressively moves deeper, for example, into the app
going from screen to screen, the StateXController object retains the sequence of State objects
its been assigned to in turn. It's 'current' state object (its .state
property) is always the one residing in the current screen.
However, as you see in the screenshots above, the controller has the means to reference 'past' State objects
from previous screens (ofState() and stateOf()). Of course, when the user retreats back to the original screen, the controller's
'current' state object property reflects that change accordingly.
Further, the StateX class has access to some 27 events functions. Thus, if a controller is registered with that State object, those events are delegated to that controller. The controller is to directly handle any such events while the State object is to be remain concerned with just the interface. This all follows the clean architecture paradigm.
No Controller Bloat
You're able to add as many controllers as you want to an individual StateX object. This prevents making your controller class too big to manage---bloating it with all the business rules required. Instead, you can break down the logic into manageable segments each representing a particular aspect of the app's business rules , in turn, each aspect could be represented by an individual controller class if applicable. Note, when a StateX object has a number of registered Controllers, and a system event occurs, for example, those controllers will run in the order they were assigned to possibly address that event---if you've supplied the code to do so.Of course, nothing is stopping you from having controllers call other controllers or other objects representing databases, or other third-party packages so to address the varying complexity of your app and its business rules. The 'controller side' of your app deals with the logic. You're free to organize the degree of abstraction and complexity necessary to do so leaving the interface to the StateX object.
page_01.dart |
---|
Control The State
Thus, when you register a controller to a StateX object, take advantage of the fact that a controller has all the functions like its corresponding StateX object. For example, if there's anything your controller may need to be initialized before a widget is displayed, remember a controller has its initState() function to initialize such requirements.
In the first screenshot below, the WordPairsTimer controller class has to set up its timer and pass its State object reference to the 'data object' called, model, before its particular State object can proceed to calling its build() function and display somthing on the screen. Since these are not operations pertaining to the interface directly, they are well-suited to reside in one of the designated controller objects.
The second screenshot below, is of another example app where it appears it's necessary for a controller to instantiate another controller called, ExampleAppController. It obviously a requirement and part of the 'business process' and so it too resides in a controller.
word_pair_timer.dart | contacts_app.dart |
---|
Further note in the first screenshot above, the controller's corresponding deactivate() function turns off the timer if and when, in this instance, that screen is closed. But that's not sufficient. Let's assume this example app is running on a mobile phone. If and when the user chooses to answer a phone call, for example, and place this app in the background, again, it's good practice to turn off the timer. The timer is re-initialized only if and when the user returns to that app. This behavior is easily achieved in that controller as well using its didChangeAppLifecycleState() and resumedLifecycleState() functions to name a few. See below.
word_pair_timer.dart |
---|
The Singleton Pattern
I find the role of a Controller is best served by a single instance of that class throughout the life of the app. It's called upon to respond to various system and user events throughout an app's lifecycle and yet still retain an ongoing and reliable state. A factory constructor for the Controller class readily accomplishes this. See below.class CounterController extends StateXController {
factory CounterController() => _this ??= CounterController._();
CounterController._() : super();
static CounterController? _this;
With the Singleton pattern, making only one single instance of a particular class creates lesser overhead. Certainly not a steadfast rule, but it's suggested all controllers instantiate with a factory constructor. Again, doing so agrees with its general role as an ongoing custodian of the app's business rules and event handling. A clean, consistent, and manageable approach, and as it happens, one that adheres to good programming practices.
The Controller and State Events
As mentioned above, with the controller, you not only supply the app's business rules, but can also respond to the device and system events that commonly occur during an app's lifecycle. The sample code below lists all the available 'event' functions. In most cases, you'll only use the functions, initAsync(), initState(), deactivate(), and dispose() as well as its 'lifecycle' functions. However, they're all there for you as you may need them someday in a future app:
(Below is the Controller from the 'Three Page' Counter Example App.)
class CounterController extends StateXController {
factory CounterController() => _this ??= CounterController._();
CounterController._() : super();
static CounterController? _this;
/// The framework will call this method exactly once.
/// Only when the [StateX] object is first created.
@override
void initState() {
super.initState();
}
/// Called to complete any asynchronous operations.
@override
Future<bool> initAsync() async {
return true;
}
/// The framework calls this method when the [StateX] object removed from widget tree.
/// i.e. The screen is closed.
@override
void deactivate() {}
/// Called when this State object was removed from widget tree for some reason
/// Undo what was done when [deactivate] was called.
@override
void activate() {}
/// The framework calls this method when this [StateX] object will never
/// build again.
/// Note: YOU DON'T KNOW WHEN THIS WILL RUN in the Framework.
/// PERFORM ANY TIME-CRITICAL OPERATION IN deactivate() INSTEAD!
@override
void dispose() {
super.dispose();
}
/// The application is not currently visible to the user, not responding to
/// user input, and running in the background.
@override
void pausedLifecycleState() {}
/// Called when app returns from the background
@override
void resumedLifecycleState() {}
/// The application is in an inactive state and is not receiving user input.
@override
void inactiveLifecycleState() {}
/// Either be in the progress of attaching when the engine is first initializing
/// or after the view being destroyed due to a Navigator pop.
@override
void detachedLifecycleState() {}
/// Override this method to respond when the [StatefulWidget] is recreated.
@override
void didUpdateWidget(StatefulWidget oldWidget) {}
/// Called when this [StateX] object is first created immediately after [initState].
/// Otherwise called only if this [State] object's Widget
/// is a dependency of [InheritedWidget].
@override
void didChangeDependencies() {}
/// Called whenever the application is reassembled during debugging, for
/// example during hot reload.
@override
void reassemble() {}
/// Called when the system tells the app to pop the current route.
/// For example, on Android, this is called when the user presses
/// the back button.
@override
Future<bool> didPopRoute() async {
return super.didPopRoute();
}
/// Called when the host tells the app to push a new route onto the
/// navigator.
@override
Future<bool> didPushRoute(String route) async {
return super.didPushRoute(route);
}
/// Called when the host tells the application to push a new
/// [RouteInformation] and a restoration state onto the router.
@override
Future<bool> didPushRouteInformation(RouteInformation routeInformation) {
return super.didPushRouteInformation(routeInformation);
}
/// The top route has been popped off, and this route shows up.
@override
void didPopNext() {}
/// Called when this route has been pushed.
@override
void didPush() {}
/// Called when this route has been popped off.
@override
void didPop() {}
/// New route has been pushed, and this route is no longer visible.
@override
void didPushNext() {}
/// Called when the application's dimensions change. For example,
/// when a phone is rotated.
@override
void didChangeMetrics() {}
/// Called when the platform's text scale factor changes.
@override
void didChangeTextScaleFactor() {}
/// Brightness changed.
@override
void didChangePlatformBrightness() {}
/// Called when the system tells the app that the user's locale has changed.
@override
void didChangeLocales(List<Locale>? locales) {}
/// Called when the system is running low on memory.
@override
void didHaveMemoryPressure() {}
/// Called when the system changes the set of active accessibility features.
@override
void didChangeAccessibilityFeatures() {}
}
The List of Device and System Events
(Tap on each below to see the source code and a further explanation of these function.)
Classes
- AppStateXController State Object Controller
- A Controller for the 'app level'.